export type ISize = [width: number, height: number]
export type IPoint = [x: number, y: number, z: number]

//向量
type V = IPoint

function dot(a: IPoint, b: IPoint) {
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}

function add(a: IPoint, b: IPoint): IPoint {
  return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
function muti(t: number, p: IPoint): IPoint {
  return [p[0] * t, p[1] * t, p[2] * t]
}

//减法
function substract(a: IPoint, b: IPoint): IPoint {
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
function distance(p: V): number {
  return Math.sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2])
}

type T = number //系数t
function t2Point(t: T, cameraPos: IPoint, viewPoint: IPoint): IPoint {
  return add(cameraPos, muti(t, substract(viewPoint, cameraPos)))
}
//颜色
export type Color = [r: number, g: number, b: number, a: number]

/**
 * 坐标系：x,y,z;camera,viewPort;距离:像素~物理
 * - 视点/摄像机位置 pCamera:(0,0,0)
 * - viewPort 沿着z轴正向 到 pCamera 距离为 d
 *
 */

export function createViewPort(d: number, viewPortSize: ISize, viewPortPixel: ISize) {
  const pCamera: IPoint = [0, 0, 0]
  const [vWidth, vHeight] = viewPortSize
  const [vpWidth, vpHeight] = viewPortPixel
  //canvas 中{x:0,y:0} 对应 viewPoint 中 {x:-Cw/2,y:-Ch/2},且 y方向相反
  function CanvasToViewPort(x: number, y: number): IPoint {
    //返回 pO->viewPort上的点(x,y)

    x = (x * vWidth) / vpWidth - vWidth / 2 //像素距离转为物理距离
    y = -((y * vHeight) / vpHeight - vHeight / 2) //像素距离转为物理距离
    return [x, y, d]
  }
  function createTraceColor(
    traceRay: (posCamera: IPoint, posViewPort: V, from: T, to: T) => Color
  ) {
    return function traceColor(x: number, y: number): Color {
      const pViewPort = CanvasToViewPort(x, y)
      return traceRay(pCamera, pViewPort, 1, Number.MAX_VALUE)
    }
  }
  return { createTraceColor }
}

//形状
export interface ISphere {
  center: IPoint
  radius: number
  color: Color
}
function intersectRaySphere(posCamera: IPoint, cameraToViewPort: V, sphere: ISphere): T[] {
  //解二次方程： {t1,t2} = (-b (+-) Math.sqrt(b*b - 4*a*c)) / (2*a)
  const r = sphere.radius
  const CO = substract(posCamera, sphere.center)

  const a = dot(cameraToViewPort, cameraToViewPort)
  const b = 2 * dot(CO, cameraToViewPort)
  const c = dot(CO, CO) - r * r

  const discriminat = b * b - 4 * a * c
  if (discriminat < 0) {
    return []
  }
  return [(-b + Math.sqrt(discriminat)) / (2 * a), (-b - Math.sqrt(discriminat)) / (2 * a)]
}

//场景

export function creatTraceRayWithSpheres(spheres: ISphere[], bgColor: Color, lights?: Light[]) {
  return function traceRay(
    posCamera: IPoint,
    posViewPort: IPoint,
    from: number,
    to: number
  ): Color {
    type Item = { color: Color; t: T; center: IPoint }
    const intersect$: Item[] = []
    for (const sphere of spheres) {
      const t$: T[] = intersectRaySphere(posCamera, posViewPort, sphere)
      for (const _t of t$) {
        const { color, center } = sphere
        intersect$.push({ t: _t, color, center })
      }
    }
    //找最近点
    // let closest: Item = {
    //   color: bgColor,
    //   t: Number.MAX_VALUE,
    //   // t: 20,
    //   center: { x: 0, y: 0, z: 1 }
    // }
    // 打光后为什么 背景透明了？
    // - bug:lighting 太小，alpha为0
    // - fix:
    let closest: Item | null = null
    for (const item of intersect$) {
      if (item.t > from && item.t < to && item.t < (closest?.t ?? Number.MAX_VALUE)) {
        closest = item
      }
    }
    if (!closest) {
      return bgColor
    }
    if (!lights) {
      return closest.color
    }
    const P = t2Point(closest.t, posCamera, posViewPort)
    const N = substract(P, closest.center)
    const lighting = computeLighting(P, N, lights)
    const [r, g, b, a] = closest.color
    return [r * lighting, g * lighting, b * lighting, a]
  }
}

// light
export type Light = {
  type: 'ambient' | 'point' | 'directional'
  //common
  intensity: number
  //point
  position?: IPoint
  //directional
  direction?: V
}
/**
 * 光强度系数
 * @param P
 * @param N
 * @param lights
 */
function computeLighting(P: IPoint, N: V, lights: Light[]): number {
  let lighting = 0
  for (const light of lights) {
    if (light.type === 'ambient') {
      lighting += light.intensity
      continue
    }
    let L: IPoint | null
    if (light.type === 'directional') {
      L = light.direction!
    } else if (light.type === 'point') {
      L = substract(light.position!, P)
    } else {
      throw new Error('impossible type')
    }
    lighting += light.intensity * computeLighting_lightingByDirection(L, N)
  }
  return lighting
}
function computeLighting_lightingByDirection(L: V, N: V) {
  const dotNL = dot(N, L)
  //忽略掉 N,L 角度大于90度的光：负值, 物理上照不到
  if (dotNL < 0) return 0
  return dotNL / (distance(L) * distance(N))
}
